wt / Microsoft Terminal

Терминал — самое громкое опен-соурс событие Майкрософт на BUILD после первой версии WSL. Второе событие WSL2 — настоящее 4.19 Linux ядро на Hyper-V, а не подсистема как WSL. И третье событие — это C++ XAML острова для UWP приложений. Но все три связаны с терминалом, можно сказать что BUILD 2019 посвящен терминалу :-) Суть в том, что Майкрософт написала трушный терминал по спекам xterm с поддержкой DirectX рендерера, клиппинг регионами, опен соурсный Console Host, и минималистичный (86KB exe-файл) WinRT терминал с табами, который набрал 30 тыс звезд на Github.

Если вы пропустили 20 лет эволюции Microsoft API от EMM386 и Windows 16, самое время вспомнить про это потому, что речь о терминале. Вообще тот кто сталкивался с termio на UNIX вплотную, а не шапочно не мог не блевать от архитектуры, но по-своему в духе стогкольмского синдрома все мы лелеем настройки терминалов, termdb и прочие сказки динозавров. В Linux можно сказать, что референсный терминал, раотающий по спецификациям — это xterm и xterm256. В Mac все тихо замалчивают проблему, и пользуются iTerm2, потому как полный набор CUA в Apple Terminal не работает, особонно это заметно для польлзователей Midnight Commander. Поэтому хотелось увидеть такой терминал от Microsoft в котором одинаково бы звучали шифты и контролы с курсорами так нужным нам старым динозаврам с мышечной памятью для навигации рептилоидным мозгом. Я работаю в iTerm2, это самый удобный пока терминал на планете, но после использования wt я захотел сделать короткий обзор того что увидел в исходниках, возможно молодежь не знает, а деды найдут ошибки и похейтят немного! Сразу скажу терминала в котором все UTF-8 все языки мира и все шрифты работают — не существует в природе: Ubuntu Terminal, Apple Terminal, iTerm2 — все неправильно воспринимают диакритику некоторых языков, причем ошибки эти имеют такую же длинную историю как количество терминальных АПИ.

Итак, в Windows 10 существует такой фулл стек из трёх API: WinNT, Win32, WinRT, остальное — детали. Windows NT — ядро Дейва Кетлера, начатое еще в 1988 году, и являющееся вдохновляющем продолжением ОС Mach написаной в CMU (я был там!). Win32 — это 32-битное расширение 16-битного невытесняющегося ивинтлупа сишных стеков шарящего единое аддресное пространство (!), которое даже существовало для DOS/EMM386 под названием Win32s. Это всё до сих пор запускается на 10-ке. И WinRT — это старая добрая перекрашенная COM/CORBA 60-х на IDL-спеках, которая вытеснила высоколатентные влажные фантазии про векторный фидонет в лице WPF. WinNT, Win32, WinRT — святая троица АПИ Windows! Теперь обо всём по-порядку!

Win32 это публичное API которое было официальным для семейства Windows NT. Самая прогрессивная и взрослая система тогда была Windows NT 4.0 Hydra с Cytrix стеком, который сохраняет приемственность в RDP протоколах и сейчас. Это тоже Терминал, но он заслуживает отдельной статьи, там вообще космос счас, этот терминал умнее меня, там AI-кодеки для сжатия данных и т.д. Потом после Windows 2000 был некоторый застой, до рассвета, когда Марк Руссинович пришел в Microsoft и порезал KERNEL32.DLL, USE32.DLL и GDI32.DLL на api-ms-win-core стек с помощью автоматических анализаторов кода, это стало начиная с Windows 7, потом был некий шаток в сторону создания всего и вся на WPF, но когда поняли что проблемы с латенси не пофиксать, вернулись на старый добрый COM/ActiveX но назвали это WinRT, и сказали что мы работаем со всеми языками. Что нового, VBA тоже можно было из джаваскрипта вызывать еще в 2000 году. Сейчас WinRT разделяет метанформация прилоежний с CLR стеком, и шарит графический язык и его редакторы типа Microsoft Blend, с XAML серирализацией и представляет собой просто COM/DCOM/COM+/OLE/ActiveX библиотеку компонент со старыми добрыми IQueryInterface и IUnknown интерфейсами, которые кстати были еще в CORBA. DirectX и все системне сервисы тоже использую этот способ линковки, там кстати существует IDL компилятор и сериализаторы можно подставлять, покруче ваших протобафов. Я это все рассказываю потому, что исходники Microsoft Terminal испольует весь фулстек всех трех API, ABI уровня ядра Windows NT, ABI C runtime и Win32 и IDL API для COM-овским компонент WinRT и DirectX.

Скажу сразу информации в интернете про Windows Terminal и вообще консольный стек не много, почерпнуть информацию можно из имплменетации FAR и MSDN страницы по Console IO, а также в двух статьях из корпоративного блога:

Анонс Windows Terminal Кайлы Синамон
Серия постов про PTY API Рича Тёрнера

Традиционно в духе Хелен Кастер приводим список всех кто делал консольный стек в Windows:

Console Host

Эта штука со времен Windows NT существует как провайдер для Console API (аналог ncurses) клиентов типа FAR или putty. Она является главным сервером и именно в нем присутствует линковочное WinNT API для управления IOCTL на уровне ядра, а не на уровне Win32 (всё доходит до IRQ пакетов ядра). Поэтому как только команде Console API понадобится что-то суперважное, всегда можно подхачить Console Host как системный сервис. С другой стороны это же и задерживает разработку. Компоненты консольного хоста:

— InputBuffer
— OutputBuffer
— DirectX рендерер
— GDI рендерер
— VT Renderer (xterm, telnet)
— VT Interactivity (специальные оконные Win32 классы)
— Server (фронтальное Console API)
— Сопряжение и ивентинг WinNT IOCTL (бекенд)

Технически консольный хост выполнен в виде сервера который получает ввод из устройст и перенаправляет его в клиентские приложения, которые используют Console API, а теперь имеет сопряжение и с ConPTY API, которое работает на пайпках и по которых передаётся либо текст ли команды терминала, все в UTF-8. Именно для ConPTY был создан специальный headless режим. У консольного хоста conhost.exe есть следуюющие параметры:

--vtmode [xterm|xterm-256color|win-telnet]

--width [chars] --height [chars]

--signal [0xID] --server [0xID] — номера HANDLE процесса из которго будут браться потоки ввода-вывода и куда будут посылаться сигналы.

--headless — хит сезона для сопряжения с ConPTY пайпами. Создает PSEUDO_WINDOW_CLASS (WS_OVERLAPPEDWINDOW) окно для проксирования ввода в клиентские приложения, типа vim.exe без создания графического окна CONSOLE_WINDOW_CLASS (CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLK).

NOTE: Графическое окно нового хоста OpenConsole.exe который используется в новом терминале wt поддерживает DirectX и GDI рендер и табы и я надеюсь новое диалоговое окно свойств, а старый поддерживает только одно окно на цепочку вложенных консольных программ. Но даже сейчас пока еще при start в wt запускается старый conhost.exe в отдельном окне. Это должно быть пофиксано глобально на уровне системы и дефаултного терминала.

Есть еще флаг -ForceV1 — эта штука означает легаси терминалы, которые на новых виндовсах уже не работают

Главное отличие от UNIX архитектуры в том, что вместо пайпов между бинарным потоком между клиентом и сервером и termio форматером, Windows операриует IOTCL пакетами как входящим потоком, а не ESC-последовательностями, поэтому терминальный Windows стек традиционно чуть-чуть длинее чем у UNIX-ов.

Буфера и рендереры являются шаред компонентами, поэтому вы можете юзать в своих приложениях. Выбор между DirectX и GDI происходит в специальном консольном оконном классе, или другими словами, контрольном элементе Window.Interactivity.Win32 (там в оконной процедуре все хаки TranslateMessage). Протокол Server можно посмотреть в файле IoSorter. Протокол IRenderer или точнее последовательность рендеринга (RenderBase): цвета, скролинг, фон, построчная развертка, оверлеи, выделение, курсор, тайтл. IRenderEngine имеет три имплементации: WinTelnet, xterm, xterm256, которые пробрасываются как параметры командной строки и различные хуки для Paint.

— AllocConsole
— AttachConsole
— FreeConsole

Так писались такие приложения как FAR.

Функция wWinMain нового консольного хоста OpenConsole.exe имеет главный свитч для новых и старых консолей: в режиме ConhostV1.dll консольный стартует старый поток ввода-вывода ConsoleIoThread, который через DeviceIoControl читает данные из устройств. Это вообще какое-то говно мамонта.

В обычном режиме новый консольный хост по-умолчанию создает cmd.exe через CreateProcess, а Input и Ouput стримы берутся из процесса указаного в --server.

CreateProcessW(NULL, CmdLineMutable.get(), NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &StartupInformation.StartupInfo, ProcessInformation.addressof())

ConPTY

Главное отличие от UNIX архитектуры в том, что вместо пайпов между бинарным потоком между клиентом и сервером и termio форматером, Windows операриует IOTCL пакетами как входящим потоком, а не ESC-последовательностями, поэтому терминальный Windows стек традиционно чуть-чуть длинее чем у UNIX-ов. Поэтому для эмуляции терминального потока который можно передать по пайпах, было построено сопряжение с консольным хостом и представлено новое удаленный одноканальный сокет интерфейс терминала — ConPTY, который мультиплексирует текстовый поток Ground и поток управляющих последовательностей, которые структурированы и каждая группа имеет свой индикатор ESC, SCI, OSC, SS3.

Вместе VT Interactivity и VT Renderer составляют то, что является ConPTY, аналог PTY в UNIX мире. Приложения через ConPTY API имеею более короткий путь до рендерера (PowerShell например), чем через WinNT IOCTL (как обычные ANSI приложения с интерпретацией ESC последовательностей). Так этот быстрый пусть можно использовать для sshd который конектится напрямую по ConPTY API а не через Console API и IOCTL.

— CreatePseudoConsole
— ResizePseudoConsole
— ClosePseudoConsole

Теперь все будут создавать приложения через новый ConPTY API. Даные передаваться могуть через пайпы (CreatePipe), по которым передается либо текст либо бинарные управляющие последовательности (очень похоже на вебсокет :). Именно так, в UNIX стиле теперь должны общаться консольные приложения, типа ssh, vim, etc.

Virtual Terminal

Это более абстрактный слой, который тоже является шаред либами. Является имплементацией ввода-вывода, логики и ивентинга. Тут основные компоненты следующие:

— TermnialInput (keymaps)
— TerminalOutput
— MouseInput
— Terminal Parser IStateMachine

Существует две имплементации стейт имашины Input и Ouput, она является средним знаменателем всех терминалов в мире и всех контрольных последовательностей разделенных на классы состояний: TXT (Ground), ESC, SCI, OSC, SS3. Terminal Input является элементом Termninal контрола ближе к пользователю, а Terminal Output структуры являются частью сервера ближе к рендерингу, они расшифровывают [-язык и рендерят в буфер.

Для начала рассмотрим путь при котором виртуальные клавиши VK оттранслированные из NT-шных IOCTL пакетов преобразуются в терминальный поток. За это отвечает модуль TerminalInput который является полем InputBuffer — главного компонента коскольного хоста. Этот модуль содержит keymappings и работы там непочатый край!

TerminalOutput в свою очередь отвечает за преробразование спец последовательностей в подпрограммы управления буфером. Он является компонентом терминал адаптера который является компонентом SCREEN_INFORMATION, который создаётся для всех не хедлесс окон.

Terminal Control

Этот слой посвящен WinRT компонентам, контрольным элементами и свойствам приложения:

— ITerminalConnection
— ConhostConnection
— TermControl
— Terminal
— Также IDL для KeyChord, IKeyBinding

ConhostConnection устанавливает соединение с новым PTY терминалом и создает для него поток ОС. Все это является частью контрольного элемента который может вставлятся в любое приложение, например в Visual Studio Code.

CreateConPty(cmdline, startingDirectory, static_cast(_initialCols), static_cast(_initialRows), &_inPipe, &_outPipe,&_signalPipe, &_piConhost);

Windows Terminal

Windows Terminal это UWP XAML С++ остров (технически). Т.е. начиная с XAML Islands появилась возможность делать из обычных Win32 приложений UWP приложения. Именно так и написан новый терминал, и именно поэтому, чтобы его попробовать нужна версия Windows в поддержкой Win32 UWP — Windows 10 1903.

Интересно, что wt собирается на ARM64, а значит его можно поставить на Limia 950 XL, на которую уже написаны деволопер тулзы для заливания ARM64 версий Windows 10 в дуал буте! О это реально может быть фаном, использование большой Lumia с BT клавиатурой в качестве DirectX терминала. Пожалуй куплю один Lumia 950 для этого! То чего так хотели в Windows RT, а именно Win32 терминал на ARM, наконец пришёл в систему. Скоро все мы будем на ARM, на них Z3 SMT-солвер быстрее работает чем на i7.

Адрес проекта — microsoft/terminal.